package edu.mcw.rgd.clinminer.web.userrole;

import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Layout;
import com.vaadin.ui.themes.Reindeer;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.vaadin.sparklines.Sparklines;

import com.invient.vaadin.charts.InvientCharts;
import com.invient.vaadin.charts.InvientCharts.DateTimePoint;
import com.invient.vaadin.charts.InvientCharts.DateTimeSeries;
import com.invient.vaadin.charts.InvientCharts.SeriesShowEvent;
import com.invient.vaadin.charts.InvientCharts.SeriesShowListerner;
import com.invient.vaadin.charts.InvientCharts.SeriesType;
import com.invient.vaadin.charts.InvientChartsConfig;
import com.invient.vaadin.charts.InvientChartsConfig.AxisBase.AxisTitle;
import com.invient.vaadin.charts.InvientChartsConfig.DateTimeAxis;
import com.invient.vaadin.charts.InvientChartsConfig.NumberYAxis;
import com.invient.vaadin.charts.InvientChartsConfig.YAxisDataLabel;

import edu.mcw.rgd.clinminer.domain.GeneticLab;
import edu.mcw.rgd.clinminer.domain.Measurement;
import edu.mcw.rgd.clinminer.domain.Medication;
import edu.mcw.rgd.clinminer.domain.common.Persistable;
import edu.mcw.rgd.clinminer.domain.common.Recordable;
import edu.mcw.rgd.clinminer.web.utils.MeasurementConverter;

@SuppressWarnings("rawtypes")
public class ChronologicalItem {
	private static final Logger log = Logger.getLogger(ChronologicalItem.class);

	private Recordable recordable;

	private Layout contentLayout;
	private Layout fullLayout;
	private Button filteredButton;

	private Boolean isFilteredVisible;
	private static CheckBox filteringCheckBox;

	// an static list of all recordable types currently visible
	private static Set<Class> visibleClasses;

	// an inner list of all chronological items created so far
	private static List<ChronologicalItem> items;

	// Stats etc.
	private float floatValue;
	private static Map<String, List<Double>> groupValues;
	private static Map<String, Double> groupSD;
	private static Map<String, Double> groupMean;
	private String groupKey;

	private static Map<String, SortedMap<Integer, Set<ChronologicalItem>>> filteringScores;

	public ChronologicalItem(ChronologicalItem item) {
		if (item.recordable instanceof Measurement) {
			recordable = new MeasurementConverter(item.recordable);
		} else {
			recordable = item.recordable;
		}
		contentLayout = item.contentLayout;
		fullLayout = item.fullLayout;
	}

	public Recordable getRecordable() {
		return recordable;
	}

	public ChronologicalItem(Recordable r, Layout content, Layout full) {
		if (r instanceof Measurement) {
			recordable = new MeasurementConverter(r);
		} else {
			recordable = r;
		}
		contentLayout = content;
		fullLayout = full;
		items.add(this);

		// note this will be overwritten by smartfiltering later if needed
		filteredButton = new Button("");

		// add to filtering map, use of a sortedMap
		SortedMap<Integer, Set<ChronologicalItem>> stratifiedItems = filteringScores
				.get(getFilteringKey());
		if (stratifiedItems == null) {
			stratifiedItems = new TreeMap<Integer, Set<ChronologicalItem>>();
			filteringScores.put(getFilteringKey(), stratifiedItems);
		}
		Set<ChronologicalItem> itemLayer = stratifiedItems.get(r
				.getEvidenceScore());
		if (itemLayer == null) {
			itemLayer = new HashSet<ChronologicalItem>();
			stratifiedItems.put(r.getEvidenceScore(), itemLayer);
		}
		itemLayer.add(this);

	}

	public Float getFloatValue() {
		return floatValue;
	}

	public Long getPersistableId() {
		return ((Persistable) recordable).getId();
	}

	public void setFloatValue(Float value, String key) {
		this.floatValue = value;
		List<Double> values = groupValues.get(key);
		if (values == null) {
			values = new ArrayList<Double>();
			groupValues.put(key, values);
		}
		values.add(value.doubleValue());
		groupKey = key;
	}

	private Double getMean() {
		if (groupMean.get(groupKey) == null) {
			Double sum = 0d;
			if (groupValues.get(groupKey) == null) {
				return 0d;
			}
			for (Double d : groupValues.get(groupKey)) {
				sum += d;
			}
			Double mean = sum / groupValues.get(groupKey).size();
			groupMean.put(groupKey, mean);
		}
		return groupMean.get(groupKey);
	}

	private Double getSD() {
		if (groupSD.get(groupKey) == null) {
			Double sumOfDeviationsSquared = 0d;
			for (Double d : groupValues.get(groupKey)) {
				sumOfDeviationsSquared += Math.pow(getMean() - d, 2d);
			}
			Double SD = Math.sqrt(sumOfDeviationsSquared
					/ (groupValues.get(groupKey).size() - 1));
			groupSD.put(groupKey, SD);
		}
		return groupSD.get(groupKey);
	}

	public Double getMinRange() {
		return getMean() - getSD() * 2;
	}

	public Double getMaxRange() {
		return getMean() + getSD() * 2;
	}

	public static void reset(Class[] toggleClasses) {
		items = new ArrayList<ChronologicalItem>();
		groupValues = new HashMap<String, List<Double>>();
		groupMean = new HashMap<String, Double>();
		groupSD = new HashMap<String, Double>();
		filteringScores = new HashMap<String, SortedMap<Integer, Set<ChronologicalItem>>>();
		visibleClasses = new HashSet<Class>();
		visibleClasses.addAll(Arrays.asList(toggleClasses));
	}

	// a more effective way to do this?
	// can't use contains(recordable.class) as measurmenetconverter is a
	// superclass
	private Boolean isClassVisible() {
		for (Class c : visibleClasses) {
			if (c.isInstance(recordable)) {
				return true;
			}
		}
		return false;
	}

	private static Boolean isFilteringEnabled() {
		if (filteringCheckBox == null) {
			return true;
		} else {
			return (Boolean) filteringCheckBox.getValue();
		}
	}

	public Date getDate() {
		return recordable.getSingleDate();
	}

	public String getCoreLabel() {
		String coreLabel = null;
		if (recordable instanceof Measurement) {
			coreLabel = ((Measurement) recordable).getLOINCLabel();
		} else if (recordable instanceof GeneticLab) {
			coreLabel = ((GeneticLab) recordable).getTestedPhenotypeLabel();
		} else if (recordable instanceof Medication) {
			coreLabel = recordable.getOntologyTermLabel();
		} else {
			coreLabel = recordable.getOntologyTermLabel();
		}

		if (coreLabel == null) {
			coreLabel = recordable.getAnnotatedText();
		}
		return coreLabel;
	}

	@Override
	public String toString() {
		SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy");
		return "[" + sdf.format(getDate()) + "] " + getCaption();
	}

	public String getCaption() {
		String coreLabel = getCoreLabel();

		String additionalText = "";
		if (recordable instanceof Measurement) {

			if (((Measurement) recordable).getValue() != null) {
				additionalText = " " + ((Measurement) recordable).getValue();
				if (((Measurement) recordable).getUnit() != null) {
					additionalText += " "
							+ ((Measurement) recordable).getUnit();
				}
			}
		} else if (recordable instanceof Medication) {

			if (((Medication) recordable).getDailyDosage() != null) {
				additionalText = " "
						+ ((Medication) recordable).getDailyDosage();
			}
			if (((Medication) recordable).getDoseForm() != null) {
				additionalText = " [" + ((Medication) recordable).getDoseForm()
						+ "]";
			}
		}
		return coreLabel + additionalText;

	}

	public static Integer getCount(Class c) {
		Integer count = 0;
		for (ChronologicalItem item : items) {
			if (c.isInstance(item.recordable)) {
				count++;
			}
		}
		return count;
	}

	// incidentally entry point to all the smart processing
	public static void startSmartFiltering(CheckBox cb) {
		filteringCheckBox = cb;
		Map<String, List<ChronologicalItem>> sameDateItems = new HashMap<String, List<ChronologicalItem>>();

		// create sparklines this also processes measurements, means, SDs, etc.
		createSparkLines();

		// set the flag based on evidence score first
		for (SortedMap<Integer, Set<ChronologicalItem>> stratifiedItems : filteringScores
				.values()) {
			for (Entry<Integer, Set<ChronologicalItem>> e : stratifiedItems
					.entrySet()) {
				// layer with most evidence (as sorted by the sortedmap)
				Boolean visibleOnFilter = e.getKey().equals(
						stratifiedItems.lastKey());
				for (ChronologicalItem item : e.getValue()) {
					item.isFilteredVisible = visibleOnFilter;
					// need this to hide duplicate events on same date
					// but only need to deal with items that weren't filtered
					// already
					if (item.isFilteredVisible) {
						String key = item.getDate().toString()
								+ item.getFilteringKey();
						List<ChronologicalItem> list = sameDateItems.get(key);
						if (list == null) {
							list = new ArrayList<ChronologicalItem>();
							sameDateItems.put(key, list);
						}
						list.add(item);
					}
				}

			}
		}
		// hide duplicates within same day
		for (List<ChronologicalItem> list : sameDateItems.values()) {
			if (list.size() == 1) {
				continue;
			}
			// hide all
			for (ChronologicalItem item : list) {
				item.isFilteredVisible = false;
			}

			// show one
			Collections.sort(list, new Comparator<ChronologicalItem>() {
				@Override
				public int compare(ChronologicalItem item1,
						ChronologicalItem item2) {
					// if measurement sort by divergence from mean descending
					// so that first item in the list will be the most divergent
					if (item1.recordable instanceof Measurement
							&& item2.recordable instanceof Measurement) {
						Double distance1 = Math.abs(item1.getMean()
								- item1.getFloatValue());
						Double distance2 = Math.abs(item2.getMean()
								- item2.getFloatValue());
						if (distance1 == distance2) {
							return 0;
						} else if (distance1 > distance2) {
							return -1;
						} else {
							return 1;
						}

						// otherwise sort order doesn't matter
					} else {
						return 0;
					}
				}
			});

			list.get(0).isFilteredVisible = true;
		}

		// need a second full pass after all filtering was generated
		// to get correct numbers for the comment labels
		for (SortedMap<Integer, Set<ChronologicalItem>> stratifiedItems : filteringScores
				.values()) {
			for (Entry<Integer, Set<ChronologicalItem>> e : stratifiedItems
					.entrySet()) {
				for (ChronologicalItem item : e.getValue()) {
					item.createFilteredComments();
				}
			}
		}

		updateVisiblity();
	}

	public static void setVisibiltyClass(Class c, Boolean isVisible) {
		if (isVisible) {
			visibleClasses.add(c);
		} else {
			visibleClasses.remove(c);
		}
		updateVisiblity();
	}

	private Boolean shouldBeVisible() {
		// visible only if this type is visible and either filtering is disabled
		// or this item is displayed under filter
		return isClassVisible()
				&& (!isFilteringEnabled() || (isFilteringEnabled() && isFilteredVisible));
	}

	private Boolean wouldBeVisible() {
		return isFilteredVisible;
	}

	public static void updateVisiblity() {
		for (ChronologicalItem item : items) {
			Boolean wasVisible = item.contentLayout.isVisible();
			item.fullLayout.setVisible(item.shouldBeVisible());
			Boolean isVisible = item.contentLayout.isVisible();
			item.filteredButton.setVisible(isFilteringEnabled());

			// highlight if visibility changed
			if (!wasVisible && isVisible) {
				item.fullLayout.addStyleName("c-smartfiltering-highlight");
			} else { // cleanup
				item.fullLayout.removeStyleName("c-smartfiltering-highlight");
			}

		}

		setFilteringCaption();
	}

	private static void setFilteringCaption() {
		Integer total = 0;
		Integer shown = 0;

		for (ChronologicalItem item : items) {
			if (item.isClassVisible()) {
				total++;
				if (item.shouldBeVisible()) {
					shown++;
				}
			}
		}

		filteringCheckBox.setCaption(shown + " visible and " + (total - shown)
				+ " hidden");
	}

	private void createFilteredComments() {
		SortedMap<Integer, Set<ChronologicalItem>> map = filteringScores
				.get(getFilteringKey());
		// skip if it's not shown anyway
		if (!wouldBeVisible()) {
			return;
		}

		Integer hidden = 0;
		final List<ChronologicalItem> hiddenItems = new ArrayList<ChronologicalItem>();
		for (Entry<Integer, Set<ChronologicalItem>> e : map.entrySet()) {
			for (ChronologicalItem item : e.getValue()) {
				if (item != this && (!item.wouldBeVisible())) {
					hidden++;
					hiddenItems.add(item);
				}
			}
		}

		// nothing hidden don't add comments
		if (hidden == 0) {
			return;
		}

		// overwrite dummy label set on instantiation
		filteredButton = new Button();
		filteredButton.addStyleName(Reindeer.BUTTON_LINK);
		final String coreCaption = hidden + " hidden similar event"
				+ ((hidden > 1) ? "s" : "") + "</i>";
		filteredButton.setCaption("<i>Show " + coreCaption);
		filteredButton.setHtmlContentAllowed(true);
		contentLayout.addComponent(filteredButton);
		filteredButton.addListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				Boolean showing = filteredButton.getCaption().contains("Show");
				String caption = "<i> " + (showing ? "Hide" : "Show") + " "
						+ coreCaption;

				for (ChronologicalItem item : hiddenItems) {
					item.isFilteredVisible = showing;
				}

				// update captions globally
				for (Set<ChronologicalItem> set : filteringScores.get(
						getFilteringKey()).values()) {
					for (ChronologicalItem item : set) {
						if (!hiddenItems.contains(item)) {
							item.filteredButton.setCaption(caption);
						}
					}
				}

				updateVisiblity();
			}

		});
	}

	private String getFilteringKey() {
		String key = getCoreLabel().toUpperCase();
		if (recordable instanceof Measurement) {
			key += ((Measurement) recordable).getUnit();
		}
		return key;
	}

	private static void storeSparklineMeasurement(
			String key,
			ChronologicalItem item,
			Map<String, NavigableMap<Date, List<ChronologicalItem>>> measurements) {
		// unit is important to segregate keys unless a conversion is made
		// on the fly

		NavigableMap<Date, List<ChronologicalItem>> map = measurements.get(key);
		if (map == null) {
			map = new TreeMap<Date, List<ChronologicalItem>>();
			measurements.put(key, map);
		}
		List<ChronologicalItem> list = map.get(item.getDate());
		if (list == null) {
			list = new ArrayList<ChronologicalItem>();
			map.put(item.getDate(), list);
		}
		list.add(item);
	}

	private static void createSparkLines() {

		// create a list of all measurements
		// keyed by loinc label, sorted by dates, with multiple measurements per
		// day possible
		// using treemap to sort on keys alpahabetically -> systolic/diastolic
		// gets sorted in correct order for display
		Map<String, NavigableMap<Date, List<ChronologicalItem>>> measurements = new TreeMap<String, NavigableMap<Date, List<ChronologicalItem>>>();
		Map<String, String> units = new HashMap<String, String>();

		for (ChronologicalItem item : items) {
			// find measurement
			if (!(item.recordable instanceof Measurement)
					|| ((Measurement) item.recordable).getLOINCLabel() == null) {
				continue;
			}
			Measurement m = (Measurement) item.recordable;

			// convert its value, skip if uncovertable
			try {
				String key = item.getFilteringKey();

				if (m.getValue().contains("/")) {
					// clone to create double sparkline for pressure
					ChronologicalItem systolic = new ChronologicalItem(item);
					ChronologicalItem diastolic = new ChronologicalItem(item);
					systolic.setFloatValue(
							new Float(m.getValue().split("/")[0]), key
									+ "systolic");
					// z in key will make it sort after systolic
					diastolic.setFloatValue(new Float(
							m.getValue().split("/")[1]), key + "zdiastolic");
					storeSparklineMeasurement(key + "systolic", systolic,
							measurements);
					storeSparklineMeasurement(key + "zdiastolic", diastolic,
							measurements);
				} else {
					item.setFloatValue(new Float(m.getValue()), key);
					storeSparklineMeasurement(key, item, measurements);
				}

			} catch (java.lang.NumberFormatException e) {
				log.debug("Failed to convert " + m.getLOINCLabel() + " "
						+ m.getValue());
				continue;
			}

			// check units consistency
			String unitKey = m.getLOINCLabel().toLowerCase();
			String unitValue = units.get(unitKey);
			if (unitValue == null) {
				unitValue = m.getUnit();
				units.put(unitKey, unitValue);
			}
			if ((unitValue != null && !unitValue.equalsIgnoreCase(m.getUnit()))
					|| (unitValue == null && unitValue != m.getUnit())) {
				log.warn("INCONSISTENT UNIT FOR " + m.getLOINCLabel() + " >> "
						+ unitValue + " >> " + m.getUnit());
			}

		}

		for (Entry<String, NavigableMap<Date, List<ChronologicalItem>>> entry : measurements
				.entrySet()) {
			NavigableMap<Date, List<ChronologicalItem>> map = entry.getValue();
			// single result no point for sparkline
			if (map.size() < 2) {
				continue;
			}

			// create sparklines
			for (Date date : map.keySet()) {
				// start creating sparklines from second date
				if (map.headMap(date, true).size() < 2) {
					continue;
				}

				// go over all item in a date
				// log.debug("CURRENT " + date + "            "
				// + map.headMap(date, true));
				for (ChronologicalItem item : map.get(date)) {
					// /log.debug("\tFIXED "
					// + fixConcurrency(map.headMap(date, true), item));

					// log.debug("\tMEAN " + item.getMean() + "\t MIN "
					// + item.getMinRange() + "\t MAX"
					// + item.getMaxRange() + "\t SD" + item.getSD());
					Sparklines s = new Sparklines(item.getFloatValue()
							.toString());
					s.setDescription(item.recordable.getAnnotatedText());
					s.setCaption("");

					s.setValue(fixConcurrency(map.headMap(date, true), item));

					s.setAverageVisible(false);
					s.setMaxColor("#f69");
					s.setMinColor("#6f9");
					s.setDisplayRangeMin(item.getMinRange().intValue());
					s.setDisplayRangeMax(item.getMaxRange().intValue());
					s.setMinmaxLabelsVisible(false);
					s.setMinmaxDotsVisible(false);
					item.contentLayout.addComponent(s);
				}
			}
		}
	}

	private static List<Number> fixConcurrency(
			NavigableMap<Date, List<ChronologicalItem>> map,
			ChronologicalItem lastItem) {
		List<Number> result = new ArrayList<Number>();

		for (Date date : map.keySet()) {
			// average over all measurements in a date
			Float sum = 0f;
			for (ChronologicalItem item : map.get(date)) {
				sum += item.getFloatValue();
			}
			result.add(sum / map.get(date).size());
		}
		// replace last element in case it was averaged
		// effectively discarding other measurements on the same date
		result.remove(result.size() - 1);
		result.add(lastItem.getFloatValue());
		return result;
	}

	public String getType() {
		return recordable.getClass().getCanonicalName();
	}

	public static InvientCharts createChartsData(
			final InvientChartsConfig chartConfig) {
		Map<String, List<ChronologicalItem>> map = new HashMap<String, List<ChronologicalItem>>();

		// collect values
		for (ChronologicalItem item : items) {
			if (item.getFloatValue() != null
					&& groupValues.get(item.groupKey) != null) {
				List<ChronologicalItem> list = map.get(item.getFilteringKey());
				if (list == null) {
					list = new ArrayList<ChronologicalItem>();
					map.put(item.getFilteringKey(), list);
				}
				list.add(item);
			}
		}

		// LinkedHashSet<YAxis> yAxesSet = new
		// LinkedHashSet<InvientChartsConfig.YAxis>();
		final InvientCharts invChart = new InvientCharts(chartConfig);
		invChart.setSizeFull();

		for (Entry<String, List<ChronologicalItem>> entry : map.entrySet()) {
			if (entry.getValue().size() < 2) {
				continue;
			}
			DateTimeSeries seriesData = new DateTimeSeries(entry.getKey());
			NumberYAxis yAxis = new NumberYAxis();
			yAxis.setAllowDecimals(true);
			yAxis.setLabel(new YAxisDataLabel());
			yAxis.getLabel().setFormatterJsFunc(
					"function() {" + " return this.value +'°C'; " + "}");
			yAxis.setTitle(new AxisTitle(entry.getKey()));
			yAxis.setOpposite(true);
			// yAxesSet.add(yAxis);

			for (ChronologicalItem item : entry.getValue()) {
				seriesData.addPoint(new DateTimePoint(seriesData, item
						.getDate(), item.getFloatValue()));
			}
			seriesData.hide();
			seriesData.setYAxis(yAxis);
			// chartConfig.addYAxes(yAxis);
			invChart.addSeries(seriesData);
			invChart.addListener(new SeriesShowListerner() {
				@Override
				public void seriesShow(SeriesShowEvent seriesShowEvent) {
					chartConfig
							.addYAxes(seriesShowEvent.getSeries().getYAxis());
					// invChart.requestRepaint();
				}
			}, SeriesType.LINE);

		}

		DateTimeAxis xAxis = new DateTimeAxis();
		chartConfig.addXAxes(xAxis);

		// chartConfig.setYAxes(yAxesSet);
		return invChart;
	}

}
